package com.duomi.lang;

import com.duomi.lang.Lexer.Token;
import com.duomi.lang.Lexer.TokenType;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

import static com.duomi.lang.Utils.*;


@SuppressWarnings({"InnerClassMayBeStatic", "WeakerAccess", "unused"})
public class EvalEngine {

    static class Environment extends HashMap<String, Object> {

        public Environment() {
        }

        public Environment(Map<? extends String, ?> m) {
            super(m);
        }
    }

    static class EnvStack {

        private Stack<Environment> envStack = new Stack<>();

        public EnvStack() {
            addBuiltinEnv();
        }

        private void addBuiltinEnv() {
            pushNewEnv();
            for (Map.Entry<String, DuomiFunction> entry : Constants.BuiltinFunctions.entrySet()) {
                setVar(entry.getKey(), entry.getValue());
            }
            for (Class<?> clazz : Constants.BuiltinClasses) {
                setVar(clazz.getSimpleName(), clazz);
            }
        }

        Environment pushNewEnv() {
            final Environment env = new Environment();
            envStack.push(env);
            return env;
        }

        void popEnv() {
            envStack.pop();
        }

        void setVar(String name, Object value) {
            envStack.peek().put(name, value);
        }

        void updateVar(String name, Object value) {
            for (Environment env : envStack) {
                if (env.containsKey(name)) {
                    env.put(name, value);
                    return;
                }
            }

            throw new DuomiEvalException();
        }

        Object getVar(String name) {
            for (Environment env : envStack) {
                if (env.containsKey(name)) {
                    return env.get(name);
                }
            }

            throw new DuomiEvalException("no variable with name: " + name);
        }

        Stack<Environment> snapshot() {
            final Stack<Environment> stack = new Stack<>();

            //noinspection ForLoopReplaceableByForEach
            for (int i = 0; i < envStack.size(); i++) {
                stack.add(new Environment(envStack.get(i)));
            }

            return stack;
        }

        void withStack(Stack<Environment> stack, Runnable action) {
            final Stack<Environment> oldStack = this.envStack;
            envStack = stack;
            action.run();
            envStack = oldStack;
        }
    }

    final EnvStack envStack = new EnvStack();

    public EvalEngine(List<Token> builtInCode) {
        eval(builtInCode);
    }

    public Object eval(List<Token> tokens) {
        Object result = null;
        while (!tokens.isEmpty()) {
            final Token tok = peekToken(tokens);
            try {
                result = advanceAndEvalToken(tok, tokens);
            } catch (Exception e) {
                Utils.printSrcContext(tok.source, tok.row, tok.column);
                if (e instanceof DuomiEvalException) {
                    throw e;
                } else {
                    throw new DuomiEvalException("evalException", e);
                }
            }
        }
        return result;
    }

    private Object advanceAndEvalToken(Token tok, List<Token> tokens) {
        switch (tok.type) {
            case BOOL:
            case INT:
            case DOUBLE:
            case STRING:
                advanceToken(tokens);
                return tok.value;
            case DOT:
                advanceToken(tokens);
                return handleDot(tok);
            case OPEN_BLOCK:
                return handleBlock(tokens);
            case KEYWORD:
                return handleKeyword(tok.getValue(), tokens);
            case OPEN_BRACKET:
                return handleApply(tokens);
            case IDENTITY:
                advanceToken(tokens);
                return handleIdentity(tok.getValue());
        }
        throw new DuomiEvalException("can not eval Token: " + tok, tok);
    }

    private Object handleIdentity(String name) {
        return envStack.getVar(name);
    }

    private Object handleApply(List<Token> tokens) {
        Object result = null;

        final List<Object> capture = new ArrayList<>();

        advanceToken(tokens); // consume open
        while (!tryAdvance(tokens, isTokenWithType(TokenType.CLOSE_BRACKET))) {
            Token tok = peekToken(tokens);
            final Object value = advanceAndEvalToken(tok, tokens);
            result = value;
            capture.add(value);
        }
        advanceToken(tokens); // consume end

        if (!capture.isEmpty() && capture.get(0) instanceof DuomiFunction) {
            final DuomiFunction func = (DuomiFunction) capture.get(0);
            result = func.invoke(capture.subList(1, capture.size()));
        }

        return result;
    }

    private Object handleKeyword(String keyword, List<Token> tokens) {
        advanceToken(tokens);
        switch (keyword) {
            case "let":
                return handleLet(tokens);
            case "fn":
                return handleFn(tokens);
            case "set!":
                return handleSet(tokens);
            case "new":
                return handleNew();
            case "if":
                return handleIf();
            case "cond":
                return handleCond(tokens);
            case "for":
                return handleFor(tokens);
            default:
                throw new AssertionError("unknown keyword");
        }
    }

    //////////////////// Dot

    class DotFunction implements DuomiFunction {
        private final String name;

        DotFunction(String name) {
            this.name = name;
        }

        @Override
        public Object invoke(List<Object> arguments) {
            if (name.equals("class")) { // access class
                except(".class should has only 1 args", arguments.size() == 1);
                final Object obj = arguments.get(0);
                if (obj instanceof Class) {
                    return obj;
                }
                final String clsName = exceptAs(arguments.get(0), String.class);
                return Constants.findClass(clsName);
            }

            except("dot must has at least 1 args", arguments.size() >= 1);
            final Object host = arguments.get(0);
            final Class<?> clazz = host instanceof Class ? ((Class) host) : host.getClass();
            final Field field = ReflectUtils.findField(clazz, name);
            if (field != null) { // access field
                if (arguments.size() == 2) { // -- set
                    final Object value = arguments.get(1);
                    ReflectUtils.setField(host, field, value);
                    return value;
                } else if (arguments.size() == 1) { // -- get
                    return ReflectUtils.getField(host, field);
                } else {
                    throw new SyntaxException("invalid field syntax");
                }
            } else { // access method
                final List<Object> args = arguments.subList(1, arguments.size());
                return ReflectUtils.invokeMethod(host, name, args);
            }
        }
    }

    private DotFunction handleDot(Token tok) {
        return new DotFunction(tok.getValue());
    }

    //////////////////// Block

    class Block {
        private final List<Token> tokens;

        Block(List<Token> tokens) {
            this.tokens = tokens;
        }

        Object eval() {
            return eval(Collections.emptyMap());
        }

        Object eval(Map<String, Object> vars) {
            envStack.pushNewEnv();
            for (Map.Entry<String, Object> entry : vars.entrySet()) {
                envStack.setVar(entry.getKey(), entry.getValue());
            }
            try {
                return EvalEngine.this.eval(tokens);
            } finally {
                envStack.popEnv();
            }
        }
    }

    private Block handleBlock(List<Token> tokens) {
        advanceToken(tokens); // consume open
        int level = 1;

        final List<Token> capture = new ArrayList<>();

        while (level > 0) {
            final Token tok = peekToken(tokens);
            if (tok.type == TokenType.OPEN_BLOCK) {
                level += 1;
            }
            if (tok.type == TokenType.CLOSE_BLOCK) {
                level -= 1;
                if (level == 0) {
                    break;
                }
            }
            capture.add(advanceToken(tokens));
        }
        advanceToken(tokens); // consume close

        return new Block(capture);
    }

    //////////////// for

    /**
     * for list {}
     */
    class ForFunction implements DuomiFunction {
        @Override
        public Object invoke(List<Object> arguments) {
            except("for should has 2 arguments", arguments.size() == 2);
            final Object iterableObj = arguments.get(0);
            except("", iterableObj instanceof Iterable || iterableObj instanceof Map);
            final Block block = exceptAs(arguments.get(1), Block.class);

            if (iterableObj instanceof Iterable) {

                final Iterable iterable = exceptAs(iterableObj, Iterable.class);

                int index = 0;

                for (Object item : iterable) {
                    final HashMap<String, Object> vars = new HashMap<>();
                    vars.put("@value", item);
                    vars.put("@index", index);
                    block.eval(vars);

                    index += 1;
                }
            } else {
                final Map<?, ?> map = exceptAs(iterableObj, Map.class);
                for (Map.Entry<?, ?> entry : map.entrySet()) {
                    final HashMap<String, Object> vars = new HashMap<>();
                    vars.put("@key", entry.getKey());
                    vars.put("@value", entry.getValue());
                    block.eval(vars);
                }
            }

            return null;
        }
    }

    private Object handleFor(List<Token> tokens) {
        return new ForFunction();
    }

    ////////////////// cond

    /**
     * cond () {}
     * () {}
     * () {}
     * (*) {}
     */
    class CondFunction implements DuomiFunction {
        @Override
        public Object invoke(List<Object> arguments) {
            except("cond args must be paired", arguments.size() > 0 && arguments.size() % 2 == 0);

            for (int i = 0; i < arguments.size(); i += 2) {
                final Object test = arguments.get(i);
                final Block block = exceptAs(arguments.get(i + 1), Block.class);

                boolean isFullMatch = test.equals("*");
                if (isFullMatch) {
                    except("* only supported in latest cond", i == arguments.size() - 2);
                }

                if (bool(test) || isFullMatch) {
                    return block.eval();
                }
            }
            return null;
        }
    }

    private Object handleCond(List<Token> tokens) {
        return new CondFunction();
    }

    ///////////////// if

    /**
     * (if (>= 2) {} {})
     */
    class IfFunction implements DuomiFunction {
        @Override
        public Object invoke(List<Object> arguments) {
            except("if with args 2 or 3", arguments.size() == 2 || arguments.size() == 3);

            if (bool(arguments.get(0))) {
                final Block block = exceptAs(arguments.get(1), Block.class);
                return block.eval();
            } else if (arguments.size() == 3) {
                final Block block = exceptAs(arguments.get(2), Block.class);
                return block.eval();
            }

            throw new AssertionError("impossible");
        }
    }

    private Object handleIf() {
        return new IfFunction();
    }

    //////////////// fn def

    class FuncDefinition implements DuomiFunction {
        final String name;
        final List<String> parameters;
        final Block block;
        final Stack<Environment> envSnapshot;

        public FuncDefinition(String name, List<String> parameters, Block block, Stack<Environment> envSnapshot) {
            this.name = name;
            this.parameters = parameters;
            this.block = block;
            this.envSnapshot = envSnapshot;
        }

        @Override
        public Object invoke(List<Object> arguments) {
            final AtomicReference<Object> resultCapture = new AtomicReference<>(null);
            envStack.withStack(this.envSnapshot, () -> {
                except("arguments size should match with parameter size", arguments.size() == parameters.size());

                final HashMap<String, Object> vars = new HashMap<>();

                for (int i = 0; i < parameters.size(); i++) {
                    final String param = parameters.get(i);
                    final Object arg = arguments.get(i);
                    vars.put(param, arg);
                }

                final Object result = block.eval(vars);
                resultCapture.set(result);
            });
            return resultCapture.get();
        }
    }

    private Object handleFn(List<Token> tokens) {
        final String id = advanceToken(tokens, TokenType.IDENTITY);
        final List<String> params = captureParameters(tokens);
        advanceToken(tokens, TokenType.OPEN_BLOCK);
        final Block block = handleBlock(tokens);

        final Stack<Environment> snapshot = envStack.snapshot();

        final FuncDefinition func = new FuncDefinition(id, params, block, snapshot);

        envStack.setVar(id, func);

        return "<Func-" + id + " >";
    }

    private List<String> captureParameters(List<Token> tokens) {
        final List<String> parameters = new ArrayList<>();
        advanceToken(tokens, TokenType.OPEN_BRACKET); // open

        while (tryAdvance(tokens, token -> token.type != TokenType.CLOSE_BRACKET)) {
            final String param = advanceToken(tokens, TokenType.IDENTITY);
            parameters.add(param);
        }

        advanceToken(tokens, TokenType.CLOSE_BRACKET); // close
        return parameters;
    }

    //////////////////// New

    class NewFunc implements DuomiFunction {
        @Override
        public Object invoke(List<Object> arguments) {
            except("new function must has args size > 1", arguments.size() >= 1);

            final Class<?> clazz;
            final Object obj = arguments.get(0);
            if (obj instanceof Class) {
                clazz = ((Class) obj);
            } else {
                final String className = exceptAs(obj, String.class);
                clazz = Constants.findClass(className);
            }

            final List<Object> args = arguments.subList(1, arguments.size());
            return ReflectUtils.invokeConstructor(clazz, args);
        }
    }

    /**
     * (new StringBuilder)
     * (new StringBuilder "hello")
     */
    private Object handleNew() {
        return new NewFunc();
    }

    ////////////////////// set!

    /**
     * special
     * (set! name value)
     */
    private Object handleSet(List<Token> tokens) {
        final String id = advanceToken(tokens, TokenType.IDENTITY);
        final Token tok = peekToken(tokens);
        final Object value = advanceAndEvalToken(tok, tokens);
        envStack.updateVar(id, value);
        return value;
    }

    ///////////////////// special let

    /**
     * special
     * <p>
     * (let (x 1) (y 2) {})
     */
    private Object handleLet(List<Token> tokens) {
        while (bool(tryAdvance(tokens, isTokenWithType(TokenType.OPEN_BRACKET)))) {
            advanceToken(tokens);
            final String id = advanceToken(tokens, TokenType.IDENTITY);
            final Object value = advanceAndEvalToken(peekToken(tokens), tokens);
            envStack.setVar(id, value);
            advanceToken(tokens, TokenType.CLOSE_BRACKET);
        }

        if (tryAdvance(tokens, isTokenWithType(TokenType.OPEN_BLOCK))) {
            final Block block = handleBlock(tokens);
            return block.eval();
        }

        return null;
    }

    //////////////////////// utils

    private <T> T advanceToken(List<Token> tokens, TokenType type) {
        final Token tok = advanceToken(tokens);
        if (tok.type == type) {
            return tok.getValue();
        }
        throw new DuomiEvalException("except tokenType: " + type + ", but get: " + tok);
    }

    private Token advanceToken(List<Token> tokens) {
        return tokens.remove(0);
    }

    private Token peekToken(List<Token> tokens) {
        return tokens.get(0);
    }

    private boolean tryAdvance(List<Token> tokens, Predicate<Token> predicate) {
        if (tokens.isEmpty()) {
            return false;
        }
        return predicate.test(peekToken(tokens));
    }

    @SuppressWarnings("SameParameterValue")
    private static Predicate<Token> isTokenWithType(TokenType type) {
        return token -> token.type == type;
    }
}
